/* Copyright 2024 Coremail
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import type { BufferHint, IBuffer, IBufferCreator } from 'coremail';
import { getLogger } from 'coremail';
import fs from '@ohos.file.fs';
import util from '@ohos.util';
import { ExtVsMimeDict } from '../utils/MimeExtMap';
import { encodeUtf8 } from 'coremail/src/main/ets/utils/encodings';


export class FileBufferCreator implements IBufferCreator {
  _path: string;
  _index: number = 1;
  _account: string;
  _fileTitle: string;
  constructor(path: string,fileTitle:string = null) {
    this._path = path;
    this._fileTitle = fileTitle;
  }

  createBuffer(hint?: BufferHint): IBuffer {
    let title = this._fileTitle? this._fileTitle : this._index;
    let truePath = this._path + "/" + title + ".buf";
    if (hint["type"] == "image") {
      const subType = hint["subType"] as string
      const mime = hint["type"] + "/" + subType
      const ext = Object.entries(ExtVsMimeDict).find(item => item[1] == mime)
      if (ext) {
        truePath = this._path + "/" + title + "." + ext[0];
      }
    }
    const buffer = new FileBuffer(truePath);
    this._index += 1; // TODO: 做个递增验证效果先
    return buffer;
  }
}

const READ_SIZE = 1024 * 100;
const WRITE_SIZE = 1024 * 100;
export class FileBuffer implements IBuffer {

  filePath: string;
  file:fs.File = null;

  _writeBuffer: (Uint8Array | string)[] = [];
  _writeBufferSize: number = 0;
  _writeEnd: boolean = false; // 防止 end 被多次调用

  constructor(filePath: string) {
    this.filePath = filePath;
    fs.access(filePath)
      .then(existed => {
        if (!existed) {
          this.detectAndCreateDir(filePath);
        }
      })
  }

  detectAndCreateDir(filePath: string): void {
    let pathComponents = filePath.split('/');
    pathComponents.pop();
    let dir = pathComponents[0];
    for(let i = 0; i < pathComponents.length; i ++) {
      let component = pathComponents[i];
      dir += `/${component}`;
      if(fs.accessSync(dir)){
        continue
      }
      else {
        fs.mkdirSync(dir);
      }
    }
  }

  async push(buffer: IBuffer): Promise<void> {
    for await (const chunk of buffer) {
      await this.feed(chunk);
    }
  }

  async feed(data: string | Uint8Array): Promise<void> {
    this._writeBuffer.push(data);
    this._writeBufferSize += data.length;
    if (this._writeBufferSize >= WRITE_SIZE) {
      await this._write();
    }
  }

  async _write(): Promise<void> {
    if (this._writeBuffer.length == 0) {
      return;
    }
    if (!this.file) {
      getLogger("fileBuffer").trace('start write', this.filePath);
      const file: fs.File = await fs.open(this.filePath, fs.OpenMode.WRITE_ONLY | fs.OpenMode.CREATE | fs.OpenMode.TRUNC).catch((err: unknown):fs.File  => {
        getLogger("fileBuffer").error('open', this.filePath, err);
        return null;
      });
      if (file) {
        this.file = file;
      } else {
        return;
      }
    }
    let b = new Uint8Array(this._writeBufferSize + 256); // 预留一点空间
    let offset = 0;
    for (const data of this._writeBuffer) {
      let u8a: Uint8Array;
      if (data instanceof Uint8Array) {
        u8a = data as Uint8Array;
      } else {
        u8a = encodeUtf8(data as string);
      }
      if (offset + u8a.byteLength <= b.byteLength) {
        b.set(u8a, offset);
      } else {
        const bb = new Uint8Array(b.byteLength + u8a.byteLength * 2);
        bb.set(b);
        bb.set(u8a, offset);
        b = bb;
      }
      offset += u8a.byteLength;
    }
    try {
      // getLogger("file-buffer").trace('begin', this.filePath, offset);
      await fs.write(this.file.fd, b.buffer, { length: offset });
      // getLogger("file-buffer").trace('end', this.filePath, offset);
    }
    catch (error: unknown) {
      getLogger("file-buffer").warn(error);
    }
    this._writeBuffer = [];
    this._writeBufferSize = 0;
  }

  async end(data?: string | Uint8Array): Promise<void> {
    if (this._writeEnd) {
      return;
    }
    this._writeEnd = true;
    if (data) {
      await this.feed(data);
    }
    await this._write();
    await fs.close(this.file);
    getLogger("file-buffer").trace('end write', this.filePath);
    this.file = null;
  }

  [Symbol.asyncIterator](): AsyncIterableIterator<string> {
    return this.read();
  }

  read(): AsyncIterableIterator<string> {
    if (!this.file) {
      getLogger("file-buffer").trace('start read', this.filePath);
      this.file = fs.openSync(this.filePath, fs.OpenMode.READ_ONLY)
    }

    let count = 0;
    let offset = 0;
    let end = false;
    return {
      next: async (): Promise<IteratorResult<string>> => {
        if (end) {
          return { value: undefined, done: true };
        }
        try {
          // getLogger('file-buffer').trace('start read', READ_SIZE, offset);
          let buffer = new ArrayBuffer(READ_SIZE);
          count = await fs.read(this.file.fd, buffer, {
            offset: offset,
            length: READ_SIZE
          })
          // getLogger('file-buffer').trace('end read', count, READ_SIZE);

          if (count > 0) {
            offset += count;
            const u8a = new Uint8Array(buffer, 0, count);
            const decoder = util.TextDecoder.create('utf-8');
            const str = decoder.decodeWithStream(u8a);
            return { value: str, done: false };
          }
          else {
            getLogger("file-buffer").trace('end read', this.filePath);
            await fs.close(this.file);
            end = true;
            return { value: undefined, done: true };
          }
        } catch(e) {
          getLogger('file-buffer').error('read file failed', e)
          return {done: true, value: undefined}
        }
      },
      [Symbol.asyncIterator](): AsyncIterableIterator<string> {
        return this as AsyncIterableIterator<string>;
      },
    };
  }

  async readAll(): Promise<string> {
    const iterator = this.read()
    let result: string = ""
    for await (const chunk of iterator) {
      result = result + chunk
    }
    return result
  }

  async getSize(): Promise<number> {
    let stat = await fs.stat(this.filePath) as fs.Stat;
    return stat.size
  }

  readRaw(): AsyncIterableIterator<Uint8Array> {
    if (!this.file) {
      getLogger("file-buffer").trace('start read', this.filePath);
      this.file = fs.openSync(this.filePath, fs.OpenMode.READ_ONLY)
    }

    let count = 0;
    let offset = 0;
    let end = false;
    return {
      next: async (): Promise<IteratorResult<Uint8Array>> => {
        if (end) {
          return { value: undefined, done: true };
        }
        const buffer = new ArrayBuffer(READ_SIZE);
        count = await fs.read(this.file.fd, buffer, { offset: offset, length: READ_SIZE })
        if (count > 0) {
          offset += count;
          const u8a = new Uint8Array(buffer, 0, count);
          return { value: u8a, done: false };
        } else {
          getLogger("file-buffer").trace('end read', this.filePath);
          fs.closeSync(this.file);
          end = true;
          return { value: undefined, done: true };
        }
      },
      [Symbol.asyncIterator](): AsyncIterableIterator<Uint8Array> {
        return this as AsyncIterableIterator<Uint8Array>;
      },
    };
  }

  async readAllRaw(): Promise<Uint8Array> {
    const iterator = this.readRaw()
    let result: Uint8Array = new Uint8Array()
    for await (const chunk of iterator) {
      let mergeArr = new Uint8Array(result.length + chunk.length);
      mergeArr.set(result);
      mergeArr.set(chunk, result.length);
      result = mergeArr;
    }
    return result
  }

  getSizeRaw(): Promise<number> {
    return this.getSize() // TODO：暂时
  }

}